package com.walleipt.ipt_tools.utils;

import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;

import org.apache.commons.lang3.StringUtils;

public class IPAddressUtils
{
	private final static int INADDR4SZ = 4;
	private final static int INADDR16SZ = 16;
	private final static int INT16SZ = 2;

	/*
	 * Converts IPv4 address in its textual presentation form into its numeric
	 * binary form.
	 * 
	 * @param src a String representing an IPv4 address in standard format
	 * 
	 * @return a byte array representing the IPv4 numeric address
	 */
	public static byte[] textToNumericFormatV4(String src)
	{
		if (src.length() == 0)
			return null;

		byte[] res = new byte[INADDR4SZ];
		String[] s = StringUtils.split(src,".");
		long val;
		try
		{
			switch (s.length)
			{
			case 1:
				/*
				 * When only one part is given, the value is stored directly in
				 * the network address without any byte rearrangement.
				 */

				val = Long.parseLong(s[0]);
				if (val < 0 || val > 0xffffffffL)
					return null;
				res[0] = (byte) ((val >> 24) & 0xff);
				res[1] = (byte) (((val & 0xffffff) >> 16) & 0xff);
				res[2] = (byte) (((val & 0xffff) >> 8) & 0xff);
				res[3] = (byte) (val & 0xff);
				break;
			case 2:
				/*
				 * When a two part address is supplied, the last part is
				 * interpreted as a 24-bit quantity and placed in the right most
				 * three bytes of the network address. This makes the two part
				 * address format convenient for specifying Class A network
				 * addresses as net.host.
				 */

				val = Integer.parseInt(s[0]);
				if (val < 0 || val > 0xff)
					return null;
				res[0] = (byte) (val & 0xff);
				val = Integer.parseInt(s[1]);
				if (val < 0 || val > 0xffffff)
					return null;
				res[1] = (byte) ((val >> 16) & 0xff);
				res[2] = (byte) (((val & 0xffff) >> 8) & 0xff);
				res[3] = (byte) (val & 0xff);
				break;
			case 3:
				/*
				 * When a three part address is specified, the last part is
				 * interpreted as a 16-bit quantity and placed in the right most
				 * two bytes of the network address. This makes the three part
				 * address format convenient for specifying Class B net- work
				 * addresses as 128.net.host.
				 */
				for (int i = 0; i < 2; i++)
				{
					val = Integer.parseInt(s[i]);
					if (val < 0 || val > 0xff)
						return null;
					res[i] = (byte) (val & 0xff);
				}
				val = Integer.parseInt(s[2]);
				if (val < 0 || val > 0xffff)
					return null;
				res[2] = (byte) ((val >> 8) & 0xff);
				res[3] = (byte) (val & 0xff);
				break;
			case 4:
				/*
				 * When four parts are specified, each is interpreted as a byte
				 * of data and assigned, from left to right, to the four bytes
				 * of an IPv4 address.
				 */
				for (int i = 0; i < 4; i++)
				{
					val = Integer.parseInt(s[i]);
					if (val < 0 || val > 0xff)
						return null;
					res[i] = (byte) (val & 0xff);
				}
				break;
			default:
				return null;
			}
		}
		catch (NumberFormatException e)
		{
			return null;
		}
		return res;
	}

	/*
	 * Convert IPv6 presentation level address to network order binary form.
	 * credit: Converted from C code from Solaris 8 (inet_pton)
	 * 
	 * Any component of the string following a per-cent % is ignored.
	 * 
	 * @param src a String representing an IPv6 address in textual format
	 * 
	 * @return a byte array representing the IPv6 numeric address
	 */
	public static byte[] textToNumericFormatV6(String src)
	{
		// Shortest valid string is "::", hence at least 2 chars
		if (src.length() < 2)
		{
			return null;
		}

		int colonp;
		char ch;
		boolean saw_xdigit;
		int val;
		char[] srcb = src.toCharArray();
		byte[] dst = new byte[INADDR16SZ];

		int srcb_length = srcb.length;
		int pc = src.indexOf("%");
		if (pc == srcb_length - 1)
		{
			return null;
		}

		if (pc != -1)
		{
			srcb_length = pc;
		}

		colonp = -1;
		int i = 0, j = 0;
		/* Leading :: requires some special handling. */
		if (srcb[i] == ':')
			if (srcb[++i] != ':')
				return null;
		int curtok = i;
		saw_xdigit = false;
		val = 0;
		while (i < srcb_length)
		{
			ch = srcb[i++];
			int chval = Character.digit(ch, 16);
			if (chval != -1)
			{
				val <<= 4;
				val |= chval;
				if (val > 0xffff)
					return null;
				saw_xdigit = true;
				continue;
			}
			if (ch == ':')
			{
				curtok = i;
				if (!saw_xdigit)
				{
					if (colonp != -1)
						return null;
					colonp = j;
					continue;
				}
				else if (i == srcb_length)
				{
					return null;
				}
				if (j + INT16SZ > INADDR16SZ)
					return null;
				dst[j++] = (byte) ((val >> 8) & 0xff);
				dst[j++] = (byte) (val & 0xff);
				saw_xdigit = false;
				val = 0;
				continue;
			}
			if (ch == '.' && ((j + INADDR4SZ) <= INADDR16SZ))
			{
				String ia4 = src.substring(curtok, srcb_length);
				/* check this IPv4 address has 3 dots, ie. A.B.C.D */
				int dot_count = 0, index = 0;
				while ((index = ia4.indexOf('.', index)) != -1)
				{
					dot_count++;
					index++;
				}
				if (dot_count != 3)
				{
					return null;
				}
				byte[] v4addr = textToNumericFormatV4(ia4);
				if (v4addr == null)
				{
					return null;
				}
				for (int k = 0; k < INADDR4SZ; k++)
				{
					dst[j++] = v4addr[k];
				}
				saw_xdigit = false;
				break; /* '\0' was seen by inet_pton4(). */
			}
			return null;
		}
		if (saw_xdigit)
		{
			if (j + INT16SZ > INADDR16SZ)
				return null;
			dst[j++] = (byte) ((val >> 8) & 0xff);
			dst[j++] = (byte) (val & 0xff);
		}

		if (colonp != -1)
		{
			int n = j - colonp;

			if (j == INADDR16SZ)
				return null;
			for (i = 1; i <= n; i++)
			{
				dst[INADDR16SZ - i] = dst[colonp + n - i];
				dst[colonp + n - i] = 0;
			}
			j = INADDR16SZ;
		}
		if (j != INADDR16SZ)
			return null;
		byte[] newdst = convertFromIPv4MappedAddress(dst);
		if (newdst != null)
		{
			return newdst;
		}
		else
		{
			return dst;
		}
	}

	/**
	 * @param src
	 *            a String representing an IPv4 address in textual format
	 * @return a boolean indicating whether src is an IPv4 literal address
	 */
	public static boolean isIPv4LiteralAddress(String src)
	{
		return textToNumericFormatV4(src) != null;
	}

	/**
	 * @param src
	 *            a String representing an IPv6 address in textual format
	 * @return a boolean indicating whether src is an IPv6 literal address
	 */
	public static boolean isIPv6LiteralAddress(String src)
	{
		return textToNumericFormatV6(src) != null;
	}

	/*
	 * Convert IPv4-Mapped address to IPv4 address. Both input and returned
	 * value are in network order binary form.
	 * 
	 * @param src a String representing an IPv4-Mapped address in textual format
	 * 
	 * @return a byte array representing the IPv4 numeric address
	 */
	public static byte[] convertFromIPv4MappedAddress(byte[] addr)
	{
		if (isIPv4MappedAddress(addr))
		{
			byte[] newAddr = new byte[INADDR4SZ];
			System.arraycopy(addr, 12, newAddr, 0, INADDR4SZ);
			return newAddr;
		}
		return null;
	}

	/**
	 * Utility routine to check if the InetAddress is an IPv4 mapped IPv6
	 * address.
	 * 
	 * @return a <code>boolean</code> indicating if the InetAddress is an IPv4
	 *         mapped IPv6 address; or false if address is IPv4 address.
	 */
	private static boolean isIPv4MappedAddress(byte[] addr)
	{
		if (addr.length < INADDR16SZ)
		{
			return false;
		}
		if ((addr[0] == 0x00) && (addr[1] == 0x00) && (addr[2] == 0x00)
				&& (addr[3] == 0x00) && (addr[4] == 0x00) && (addr[5] == 0x00)
				&& (addr[6] == 0x00) && (addr[7] == 0x00) && (addr[8] == 0x00)
				&& (addr[9] == 0x00) && (addr[10] == (byte) 0xff)
				&& (addr[11] == (byte) 0xff))
		{
			return true;
		}
		return false;
	}
	
	/**
	 * 
	 * @param ipnm
	 *            192.168.1.0/24
	 * @return
	 */
	public static byte[] getEndAddressOfV4(byte[] beginAddr, int cidr)
	{
		byte[] nwkAddr = cidr2NetWorkAddrV4(cidr);
		return endOfNetwork(beginAddr, nwkAddr);
	}

	/**
	 * 
	 * @param ipNm
	 *            cidr = "2001:9fe:a::/48";
	 * @return
	 */
	public static byte[] getEndAddressOfV6(byte[] beginAddr, int cidr)
	{
		byte[] nwkAddr = cidr2NetWorkAddrV6(cidr);
		return endOfNetwork(beginAddr, nwkAddr);
	}

	public static String addr2TextV4(byte[] addr)
	{
		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < addr.length; i++)
		{
			buf.append(addr[i] & 0xFF);
			if (i < addr.length - 1)
				buf.append(".");
		}
		return buf.toString();
	}

	public static String addr2TextV6(byte[] src)
	{
		int len = 8;
		StringBuffer sb = new StringBuffer(39);
		for (int i = 0; i < len; i++)
		{
			sb.append(Integer.toHexString(((src[i << 1] << 8) & 0xff00)
					| (src[(i << 1) + 1] & 0xff)));
			if (i < len - 1)
			{
				sb.append(":");
			}
		}
		return sb.toString();

	}

	/**
	 * 255.255.255.0 -> 24
	 * 
	 * @param mask
	 *            255.255.255.0
	 * @return
	 */
	public static int mask2CIDRV4(String mask)
	{
		String[] nm = mask.split("\\.", 4);
		int i = 0;
		int[] ma = new int[4];
		for (int j = 0; j < ma.length; j++)
		{
			ma[i] = Integer.parseInt(nm[i]);
			i++;
		}

		int mask1 = 0;
		for (i = 0; i < 4; i++)
		{
			mask1 += Integer.bitCount(ma[i]);
		}
		return mask1;
	}

	/**
	 * 24 -> 0xFFFFFF00
	 * 
	 * @param cidr
	 *            24
	 * @return
	 */
	public static int cidr2MaskV4(int cidr)
	{
		return ~((1 << (32 - cidr)) - 1);
	}

	public static BigInteger cidr2MaskV6(int cidr)
	{
		return BigInteger.ONE.shiftLeft(128 - cidr).subtract(BigInteger.ONE)
				.not();
	}

	/**
	 * 24 -> [0,0,0,FF]
	 * 
	 * @param cidr
	 * @return
	 */
	public static byte[] cidr2NetWorkAddrV4(int cidr)
	{
		int cidrInt = ~cidr2MaskV4(cidr);
		return int2AddrV4(cidrInt);
	}

	public static byte[] cidr2NetWorkAddrV6(int cidr)
	{
		BigInteger cidrInt = cidr2MaskV6(cidr).not();
		return int2AddrV6(cidrInt);
	}

	// ---------------------------------
	/**
	 * integer 2 addr array
	 * 
	 * @param ipInt
	 * @return
	 */
	public static byte[] int2AddrV4(int ipInt)
	{
		byte[] a = new byte[4];

		a[0] = (byte) ((ipInt >> 24) & 0xFF);
		a[1] = (byte) ((ipInt >> 16) & 0xFF);
		a[2] = (byte) ((ipInt >> 8) & 0xFF);
		a[3] = (byte) ((ipInt & 0xFF));

		return a;
	}

	public static byte[] int2AddrV6(BigInteger addr)
	{
		byte[] b = addr.toByteArray();

		if (b.length == 16)
		{
			return b;
		}

		byte[] a = new byte[16];
		if (b.length == 17)
		{
			System.arraycopy(b, 1, a, 0, 16);
		}
		else
		{
			// copy the address into a 16 byte array, zero-filled.
			int p = 16 - b.length;
			for (int i = 0; i < b.length; i++)
			{
				a[p + i] = b[i];
			}
			return a;
		}
		return null;
	}

	/**
	 * begin + network => end
	 * 
	 * @param beginAddr
	 * @param nwkAddr
	 * @return
	 */
	public static byte[] endOfNetwork(byte[] beginAddr, byte[] nwkAddr)
	{
		byte[] earr = new byte[beginAddr.length];
		for (int i = 0; i < beginAddr.length; i++)
		{
			earr[i] = (byte) ((beginAddr[i] | nwkAddr[i]) & 0xFF);
		}
		return earr;
	}

	/**
	 * 
	 * @param addr
	 *            InetAddress.getAddress()
	 * @return
	 */
	public static int integerOfV4(byte[] addr)
	{
		return ((addr[0] & 0xFF) << 24) | ((addr[1] & 0xFF) << 16)
				| ((addr[2] & 0xFF) << 8) | (addr[3] & 0xFF);
	}
	
	static String oneIpv4Address = null ;
	static Object lock = new Object() ;
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static String getOneIpv4AddressOfLocal()
	{
		if( oneIpv4Address != null )
			return oneIpv4Address ;

		synchronized( lock )
		{
			if( oneIpv4Address != null )
				return oneIpv4Address ;
			
			try
			{
				oneIpv4Address = (String)AccessController.doPrivileged( new PrivilegedExceptionAction(){
					public Object run() throws Exception
					{
						Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces() ;
						while( networks.hasMoreElements() )
						{
							NetworkInterface network = networks.nextElement() ;
							if( network.isLoopback() )
								continue ;
							Enumeration<InetAddress> addresses = network.getInetAddresses() ;
							while( addresses.hasMoreElements() )
							{
								InetAddress address = addresses.nextElement() ;
								if( address.isLoopbackAddress() )
									continue ;
								if( address instanceof Inet4Address )
									return address.getHostAddress() ;
							}
						}
						
						return InetAddress.getLocalHost().getHostName() ;
					}
					
				}) ;
			}
			catch( Exception err )
			{
			}
		}
		
		return oneIpv4Address ;
	}
	
}
